package calao.client.ui;

import java.util.Vector;

import calao.client.Desktop;
import calao.client.ServiceCallback;
import calao.client.i18n.Text;

import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.ChangeListener;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.ClickListener;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.KeyboardListener;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.SourcesChangeEvents;
import com.google.gwt.user.client.ui.TextBoxBase;
import com.google.gwt.user.client.ui.Widget;

public class Form
extends Composite
implements ClickListener, ChangeListener, KeyboardListener
{

	static Form lastLoadedForm;

	/** Constant for view action.
	 */
	public final static int VIEW = 0;

	/** Constant for change action.
	 */
	public final static int CHANGE = 1;

	/** Constant for create action.
	 */
	public final static int CREATE = 2;

	/** Constant for search action.
	 */
	public final static int SEARCH = 3;

	/** Constant for execute action.
	 */
	public final static int EXECUTE = 4;

	// form states, see diagram "doc-files/form-states.png"
	private final static int STARTED = 0; 
	private final static int EDITED = 1; 
	private final static int CANCELED = 2; 
	private final static int VALIDATED = 3; 
	private final static int INVALIDATED = 4; 

	class Field {
		final Widget editor;
		final Widget viewer;
		final String label;
		Field(Widget editor, Widget viewer, String label) {
			this.editor = editor;
			this.viewer = viewer;
			this.label = label;
		}
		Widget getWidget() {
			if (editor==null) return viewer;
			if (action!=VIEW) return editor;
			else {
				if (editor instanceof HasDeferredLoading) {
					ServiceCallback.defer(new Command() {
						public void execute() {
							refreshViewer();
						}
					});
				}
				else refreshViewer();
				return viewer;
			}
		}
		void refreshViewer() { }
	}

	class FormButton extends Button 
	{
		FormButton(String text, boolean enabled) {
			super(text);
			addClickListener(Form.this);
			buttonPanel.add(this);
			setEnabled(enabled);
		}
	}

	final static Text text = calao.client.Desktop.getDesktop().text;

	final SpacerPanel panel = new SpacerPanel(SpacerPanel.VERTICAL);
	final FastTable fieldPanel = new FastTable(2);
	final SpacerPanel buttonPanel = new SpacerPanel(SpacerPanel.HORIZONTAL);

	FormListener[] listeners = new FormListener[0];

	final Vector fields = new Vector();

	int action;
	boolean changeAllowed;
	boolean buttonsEnabled = true;

	int state = STARTED; 
	FormButton changeButton, submitButton, cancelButton;

	public Form()
	{
		this(CHANGE);
	}

	public Form(int action)
	{
		this(action, action!=VIEW);
	}

	public Form(int action, boolean changeAllowed) 
	{
		panel.add(fieldPanel);
		initWidget(panel);
		addStyleName("calao-Form");
		fieldPanel.addStyleName(Styles.FIELD_PANEL);
		this.changeAllowed = changeAllowed;
		setAction(action);
	}

	/** Adds an editable field.
	 * This method provides auto-built viewers for a limited set of editing widgets. Such widgets 
	 * will be used in edit mode only. In view mode the form will show appropriate text reflecting
	 * widget-associated values. This text can be refreshed with {@link #refresh()}.
	 * Other widgets will be used in both view and edit mode. If they implement the {@link Editable} 
	 * interface, the form will call {@link Editable#setEditEnabled(boolean)} on them when switching
	 * from one mode to the other
	 */
	public void add(String label, Widget widget)
	{
		Field field = null;
		if (widget instanceof TextBoxBase) {
			field = new Field (widget, new FlexibleText(), label) {
				TextBoxBase box = (TextBoxBase)editor;
				void refreshViewer() {
					((FlexibleText)viewer).setText(box.getText());
				}
			};
		}
		else if (widget instanceof CheckBox) {
			field = new Field (widget, new Label(), label) {
				CheckBox box = (CheckBox)editor;
				void refreshViewer() {
					((Label)viewer).setText(box.isChecked()?	text.yes() : text.no());
				}
			};
		}	
		else if (widget instanceof ListBox) {
			field = new Field (widget, new Label(), label) {
				ListBox box = (ListBox)editor;
				void refreshViewer() {
					ListText text = new ListText();
					for (int i=0; i<box.getItemCount(); i++) {
						if (box.isItemSelected(i)) text.add(box.getItemText(i));
					}
					((Label)viewer).setText(text.getText());
				}
			};
		}		
		else if (widget instanceof NumberedListBox) {
			field = new Field (widget, new Label(), label) {
				NumberedListBox box = (NumberedListBox)editor;
				void refreshViewer() {
					((Label)viewer).setText(box.getSelectedListString());
				}
			};
		}		
		else if (widget instanceof DateBox) {
			field = new Field (widget, new Label(), label) {
				DateBox box = (DateBox)editor;
				void refreshViewer() {
					((Label)viewer).setText(box.getText());
				}
			};
		}		
		else {
			if (widget instanceof Editable) {
				Editable editableWidget = (Editable)widget;
				editableWidget.setEditEnabled(action!=VIEW);
			}
			field = new Field(widget, widget, label);
		}
		add(field);
	}

	/** Adds an editable field.
	 */
	public void add(String label, Widget editor, Widget viewer)
	{
		add(new Field(editor, viewer, label));
	}

	/** Adds a non-editable field.
	 */
	public void addNonEditable(String label, Widget viewer)
	{
		add(new Field(null, viewer, label));
	}

	public void add(String label, String text)
	{
		addNonEditable(label, new Label(text));
	}

	public void add(String label, int number)
	{
		add(label, Integer.toString(number));
	}

	private void add(Field field)
	{
		fields.add(field);
		if (field.editor!=null) addChangeSource(field.editor);
		if (fieldPanel.getRowCount()>0) {
			Elements.setHeight(fieldPanel.addEmptyCell(2), SpacerPanel.DEFAULT_SPACE_SIZE);
		}
		if (field.label!=null) {
			fieldPanel.addCell(field.label, 1, Styles.LABEL);
			fieldPanel.addCell(field.getWidget(), 1, Styles.FIELD);
		}
		else {
			fieldPanel.addCell(field.getWidget(), 2, Styles.FIELD);
		}
	}

	private void addChangeSource(Widget widget)
	{
		if (widget instanceof TextBoxBase) {
			((TextBoxBase)widget).addKeyboardListener(this);
		}
		else if (widget instanceof SourcesChangeEvents) {
			((SourcesChangeEvents)widget).addChangeListener(this);
		}	
		else if (widget instanceof CheckBox) {
			((CheckBox)widget).addClickListener(this);
		}
		else throw new IllegalArgumentException(widget.toString());
	}

	final public void onClick(Widget sender)
	{
		if (sender==changeButton) setAction(CHANGE);
		else if (sender==submitButton) validateForm();
		else if (sender==cancelButton) setState(CANCELED);
		// case of a clicked check-box
		else onChange(sender);
	}

	/** Notifies this form that changes were saved.
	 */
	public void setChangesSaved()
	{
		setEditButtonsEnabled(false);
		Desktop.getDesktop().setGreenStatus(text.changesSaved());
		for (int i=0; i<listeners.length; i++) listeners[i].onChangesSaved(Form.this);
	}

	/** Creates a new service callback that calls {@link #setChangesSaved()}.
	 */
	public ServiceCallback newSaveChangesCallback()
	{
		return new ServiceCallback() {
			protected void onReturn(Object result) {
				setChangesSaved();
			}			
		};
	}

	final public void onChange(Widget sender) 
	{
		switch (state) {
		case STARTED:
		case INVALIDATED:
		case CANCELED:
		case VALIDATED:
			setState(EDITED);
		}
	}

	private void setState(int state)
	{
		this.state = state;
		switch (state) {
		case EDITED:
			Desktop.getDesktop().clearStatus();
			setEditButtonsEnabled(true);
			for (int i=0; i<listeners.length; i++) listeners[i].onChangesAvailable(this);
			break;
		case CANCELED:
			cancelChanges();
			Desktop.getDesktop().clearStatus();
			setEditButtonsEnabled(false);
			for (int i=0; i<listeners.length; i++) listeners[i].onChangesCanceled(this);
			break;
		case VALIDATED:
			performAction();
			break;
		case INVALIDATED:
			break;
		}
	}

	private void setEditButtonsEnabled(boolean enabled)
	{
		if (submitButton!=null) submitButton.setEnabled(enabled);
		if (cancelButton!=null) cancelButton.setEnabled(enabled);
	}

	private void performAction()
	{
		switch (action) {
		case CHANGE:
			saveChanges();
			break;
		case CREATE:
			create();
			break;
		case SEARCH:
			search();
			break;
		case EXECUTE:
			execute();
			break;
		default:
			throw new RuntimeException();
		}
	}

	public void saveChanges() { }

	/** Cancels changes and resets field values.
	 * This default implementation simply calls {@link #reset()}. 
	 */
	public void cancelChanges() 
	{
		reset();
	}

	/** Resets field values.
	 */
	public void reset() { }

	/** Creates a record based on this form.
	 */
	public void create() { }

	/** Performs a search based on this form.
	 */
	public void search() { }

	/** Executes an operation based on this form.
	 */
	public void execute() { }

	public void addListener(FormListener listener)
	{
		listeners = (FormListener[])FormListener.ARRAYS.add(listeners, listener);
	}

	final public void onKeyDown(Widget sender, char keyCode, int modifiers) { }

	final public void onKeyPress(Widget sender, char keyCode, int modifiers) { }

	final public void onKeyUp(Widget sender, char keyCode, int modifiers) 
	{
		onChange(sender);
	}

	public boolean isChangeAllowed() {
		return changeAllowed;
	}

	public void setChangeAllowed(boolean changeAllowed) 
	{
		this.changeAllowed = changeAllowed;
		updateButtons();
	}

	private void updateButtons()
	{
		buttonPanel.clear();
		changeButton = submitButton = cancelButton = null;
		if (buttonsEnabled) {
			switch (action) {
			case VIEW:
				if (changeAllowed) {
					changeButton = new FormButton(text.edit(), true);
				}
				break;
			case CHANGE:
				submitButton = new FormButton(text.saveChanges(), false);
				cancelButton = new FormButton(text.cancelChanges(), false);
				break;
			case CREATE:
				submitButton = new FormButton(text.create(), true);
				break;
			case SEARCH:
				submitButton = new FormButton(text.doSearch(), true);
				break;
			case EXECUTE:
				submitButton = new FormButton(text.execute(), true);
				break;
			}
		}
		if (buttonPanel.getWidgetCount()==0) panel.remove(buttonPanel);
		else if (!buttonPanel.isAttached()) panel.add(buttonPanel);
	}

	/** Sets the target action for this form.
	 */
	public void setAction(int action) 
	{
		this.action = action;
		updateButtons();
		for (int i=0; i<fields.size(); i++) {
			Field field = getField(i);
			Widget widget = field.getWidget();
			if (widget instanceof Editable) {
				((Editable)widget).setEditEnabled(action!=VIEW);
			}
			if (field.label!=null) {
				fieldPanel.setText(i*2, 0, field.label);
				fieldPanel.setWidget(i*2, 1, widget);
			}
			else {
				fieldPanel.setWidget(i*2, 0, widget);
			}				
		}		
	}

	/** Refreshes this form.
	 * This method must be called when editors added with {@link #add(String, Widget)} are 
	 * modified, in order to refresh associated view-mode texts.
	 */
	public void refresh()
	{
		if (action==VIEW) {
			for (int i=0; i<fields.size(); i++) {
				getField(i).getWidget();
			}
		}
	}

	public void onLoad()
	{
		lastLoadedForm = this;
	}

	public void onUnload()
	{
		if (lastLoadedForm==this) lastLoadedForm = null;
	}

	public int getAction()
	{
		return action;
	}

	private Field getField(int i)
	{
		return (Field)fields.get(i);
	}

	private boolean validateForm()
	{
		try {
			for (int i=0; i<fields.size(); i++) {
				Widget editor = getField(i).editor;
				if (editor!=null && editor instanceof Validatable) {
					((Validatable)editor).validate();
				}
			}	
			validate();
			setState(VALIDATED);
			return true;
		}
		catch (InvalidInputException e) {
			Desktop.getDesktop().setYellowStatus(e.getMessage());
			setState(INVALIDATED);
			return false;			
		}
	}

	public void validate()
	throws InvalidInputException
	{}

	public void setButtonsEnabled(boolean enabled)
	{
		buttonsEnabled = enabled;
		updateButtons();
	}

	public boolean submit()
	{
		switch (state) {
		case STARTED:
		case EDITED:
		case VALIDATED:
			return validateForm();
		default:
			return false;
		}
	}

}
